/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Haystack Software Inc. All rights reserved.
 *  Licensed under the PolyForm Strict License 1.0.0. See License.txt in the project root for
 *  license information.
 *--------------------------------------------------------------------------------------------*/

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See code-license.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import {
  AsyncIterableObject,
  CancelableAsyncIterableObject,
  createCancelableAsyncIterable,
  RunOnceScheduler,
} from "vs/base/common/async"
import { CancellationToken } from "vs/base/common/cancellation"
import { onUnexpectedError } from "vs/base/common/errors"
import { Emitter } from "vs/base/common/event"
import { Disposable } from "vs/base/common/lifecycle"
import { ICodeEditor } from "vs/editor/browser/editorBrowser"
import { EditorOption } from "vs/editor/common/config/editorOptions"

export interface IHoverComputer<T> {
  /**
   * This is called after half the hover time
   */
  computeAsync?: (token: CancellationToken) => AsyncIterableObject<T>
  /**
   * This is called after all the hover time
   */
  computeSync?: () => T[]
}

const enum HoverOperationState {
  Idle,
  FirstWait,
  SecondWait,
  WaitingForAsync = 3,
  WaitingForAsyncShowingLoading = 4,
}

export const enum HoverStartMode {
  Delayed = 0,
  Immediate = 1,
}

export const enum HoverStartSource {
  Mouse = 0,
  Keyboard = 1,
}

export class HoverResult<T> {
  constructor(
    public readonly value: T[],
    public readonly isComplete: boolean,
    public readonly hasLoadingMessage: boolean,
  ) {}
}

/**
 * Computing the hover is very fine tuned.
 *
 * Suppose the hover delay is 300ms (the default). Then, when resting the mouse at an anchor:
 * - at 150ms, the async computation is triggered (i.e. semantic hover)
 *   - if async results already come in, they are not rendered yet.
 * - at 300ms, the sync computation is triggered (i.e. decorations, markers)
 *   - if there are sync or async results, they are rendered.
 * - at 900ms, if the async computation hasn't finished, a "Loading..." result is added.
 */
export class HoverOperation<T> extends Disposable {
  private readonly _onResult = this._register(new Emitter<HoverResult<T>>())
  public readonly onResult = this._onResult.event

  private readonly _firstWaitScheduler = this._register(
    new RunOnceScheduler(() => this._triggerAsyncComputation(), 0),
  )
  private readonly _secondWaitScheduler = this._register(
    new RunOnceScheduler(() => this._triggerSyncComputation(), 0),
  )
  private readonly _loadingMessageScheduler = this._register(
    new RunOnceScheduler(() => this._triggerLoadingMessage(), 0),
  )

  private _state = HoverOperationState.Idle
  private _asyncIterable: CancelableAsyncIterableObject<T> | null = null
  private _asyncIterableDone: boolean = false
  private _result: T[] = []

  constructor(
    private readonly _editor: ICodeEditor,
    private readonly _computer: IHoverComputer<T>,
  ) {
    super()
  }

  public override dispose(): void {
    if (this._asyncIterable) {
      this._asyncIterable.cancel()
      this._asyncIterable = null
    }
    super.dispose()
  }

  private get _hoverTime(): number {
    return this._editor.getOption(EditorOption.hover).delay
  }

  private get _firstWaitTime(): number {
    return this._hoverTime / 2
  }

  private get _secondWaitTime(): number {
    return this._hoverTime - this._firstWaitTime
  }

  private get _loadingMessageTime(): number {
    return 3 * this._hoverTime
  }

  private _setState(
    state: HoverOperationState,
    fireResult: boolean = true,
  ): void {
    this._state = state
    if (fireResult) {
      this._fireResult()
    }
  }

  private _triggerAsyncComputation(): void {
    this._setState(HoverOperationState.SecondWait)
    this._secondWaitScheduler.schedule(this._secondWaitTime)

    if (this._computer.computeAsync) {
      this._asyncIterableDone = false
      this._asyncIterable = createCancelableAsyncIterable((token) =>
        this._computer.computeAsync!(token),
      )
      ;(async () => {
        try {
          for await (const item of this._asyncIterable!) {
            if (item) {
              this._result.push(item)
              this._fireResult()
            }
          }
          this._asyncIterableDone = true

          if (
            this._state === HoverOperationState.WaitingForAsync ||
            this._state === HoverOperationState.WaitingForAsyncShowingLoading
          ) {
            this._setState(HoverOperationState.Idle)
          }
        } catch (e) {
          onUnexpectedError(e)
        }
      })()
    } else {
      this._asyncIterableDone = true
    }
  }

  private _triggerSyncComputation(): void {
    if (this._computer.computeSync) {
      this._result = this._result.concat(this._computer.computeSync())
    }
    this._setState(
      this._asyncIterableDone
        ? HoverOperationState.Idle
        : HoverOperationState.WaitingForAsync,
    )
  }

  private _triggerLoadingMessage(): void {
    if (this._state === HoverOperationState.WaitingForAsync) {
      this._setState(HoverOperationState.WaitingForAsyncShowingLoading)
    }
  }

  private _fireResult(): void {
    if (
      this._state === HoverOperationState.FirstWait ||
      this._state === HoverOperationState.SecondWait
    ) {
      // Do not send out results before the hover time
      return
    }
    const isComplete = this._state === HoverOperationState.Idle
    const hasLoadingMessage =
      this._state === HoverOperationState.WaitingForAsyncShowingLoading
    this._onResult.fire(
      new HoverResult(this._result.slice(0), isComplete, hasLoadingMessage),
    )
  }

  public start(mode: HoverStartMode): void {
    if (mode === HoverStartMode.Delayed) {
      if (this._state === HoverOperationState.Idle) {
        this._setState(HoverOperationState.FirstWait)
        this._firstWaitScheduler.schedule(this._firstWaitTime)
        this._loadingMessageScheduler.schedule(this._loadingMessageTime)
      }
    } else {
      switch (this._state) {
        case HoverOperationState.Idle:
          this._triggerAsyncComputation()
          this._secondWaitScheduler.cancel()
          this._triggerSyncComputation()
          break
        case HoverOperationState.SecondWait:
          this._secondWaitScheduler.cancel()
          this._triggerSyncComputation()
          break
      }
    }
  }

  public cancel(): void {
    this._firstWaitScheduler.cancel()
    this._secondWaitScheduler.cancel()
    this._loadingMessageScheduler.cancel()
    if (this._asyncIterable) {
      this._asyncIterable.cancel()
      this._asyncIterable = null
    }
    this._result = []
    this._setState(HoverOperationState.Idle, false)
  }
}
